﻿var validatorApp = {
    'results': { 'errors': 0, 'warnings': 0, 'successes': 0, 'recommendations': 0, 'aborteds': 0 },
    'currentJob': null,
    'jobsInQ': [],
    'currJobDetail': null,
    'currJobNote': null,
    'panes': 0,
    'LoadResultsTimeoutObj': null
};
var validatorStat = {
    'totalRequestsSent': 0
};
var validatorConf = {
    'perRequestDelay': 5000,
    'maxRequests': 24,
    'SpecificationUri': "http://www.odata.org/media/16352/[ms-odata].pdf",
    'payload_text_help': "paste here the response payload in atompub/xml or json format in full",
    'meta_text_help': "paste here the metadata document content in xml format in full if you have one",
    'info_error_resourse':
        "Attention: This is an OData error payload resource. If you meant to validate the error payload against OData spec, please see the validation results as reported below; otherwise, please correct the Uri input and try again.",
    'info_other_odata_resourse':
        "Attention: This is an arbitrary resource seemed to be generated by an OData producer. If you meant to validate it as an OData resource against OData spec, please see the validation results as reported below; otherwise, please correct the Uri input and try again.",
    'info_other_resourse':
        "Attention: This is an arbitrary resource; seems not a valid OData resource. If you meant to validate it as an OData resource against OData spec, please see the validation results as reported below; otherwise, please correct the Uri input and try again."
};

$(function () {
    Reset();
    InitClickHandlers();
});

function InitClickHandlers() {
    $("#liveValidateButton").click(function () { submitValidationJobByUri(); });
    $("#offlineValidateButton").click(function () { submitValidationJobByDirectText(); });
    $("#meta_icon").click(function (ev) { evh_enableCollapse(ev); });
}

// global error handlers
OData.defaultError = function (err) {
    try {
        if (err.response.statusCode == 509) {
            DisplayStatusText("Payload is too large - please retry with another URI");
        } else if (err.response.statusCode == 403) {
            DisplayStatusText("Unsupported Uri scheme - please specify a URI using http or https scheme");
        } else {
            DisplayStatusText("Error retrieving validation results - please retry.");
        }
    }
    catch (e) {
        DisplayStatusText("Error retrieving validation results - please retry.");
    }

    enableTabSwitch();
};


// status management methods
function resetJobStat() {
    validatorStat.totalRequestsSent = 0;
}

function resetApp() {
    validatorApp.jobsInQ = [];
    validatorApp.panes = 0;
    resetJob();
}

function resetJob() {
    validatorApp.results = { 'errors': 0, 'warnings': 0, 'successes': 0, 'recommendations': 0, 'aborteds': 0, 'notApplicables': 0 };
    validatorApp.currentJob = null;
    validatorApp.currJobDetail = null;
    validatorApp.currJobNote = null;
    if (validatorApp.LoadResultsTimeoutObj) {
        clearTimeout(validatorApp.LoadResultsTimeoutObj);
        validatorApp.LoadResultsTimeoutObj = null
    }

    resetJobStat();
}

function Reset() {
    ClearDisplay()
    resetApp();
    $('#infotop #statusInfo').hide();
}

function OnStart() {
    Reset();
    $('#infotop #statusInfo').show();
    disableTabSwitch();
}

function alertSendoutRequest() {
    DisplayStatusText("Sending validation request...");
}

function alertReceiveResponse() {
    DisplayStatusText("Retrieving response payload...");
}

function alertTimeout() {
    updateJobStatus("Operation timed out. Please retry.");
}

function alertJobComplete() {
    DisplayStatusText("Validation complete.");
}

function alertPayloadInProgress() {
    DisplayStatusText("Loading source payload...");
}

function alertResultsInProgress() {
    DisplayStatusText("Loading validation results...");
}


function onJobEnd() {
    alertJobComplete();
    displayJobSummary();
    if (jobHasAnyIssue()) {
        //color code to highlight
        jQuery('#summary', validatorApp.currJobDetail).addClass("highlit");
    }
    validatorApp.currJobDetail = null;
    validatorApp.currJobNote = null;
}

function OnEnd() {
    enableTabSwitch();
}

// validation controlling methods
function submitValidationJobByUri() {
    OnStart();
    //    disableTabSwitch();
    if ($('#crawling').is(':checked')) {
        submitCrawlingValidationJob();
    } else {
        submitLiveValidationJob();
    }
};

function submitValidationJobByDirectText() {
    OnStart();
    var request = {
        method: "POST",
        requestUri: "odatavalidator/ExtValidationJobs",
        data: { PayloadText: GetPayloadText(), MetadataText: GetMetadataText(), ReqHeaders: GetReqHeaders() }
    }
    OData.request(request, function (data) {
        alertReceiveResponse();

        validatorApp.jobsInQ = [];
        validatorApp.currentJob = { 'Uri': data.Uri, 'ResourceType': 0, 'Id': data.ID, 'RuleCount': data.RuleCount };
        $('#infotop #statusInfo').empty();
        validatorApp.currJobDetail = newRegularValidation(validatorApp.currentJob);
        LoadPayload(validatorApp.currentJob.Id);
    });
    alertSendoutRequest()
}

function newValidation(newJob, isSimpleJob) {
    newJob.anchor = "pane" + ++validatorApp.panes;
    var link = CreateLink(ToTypeDesc(newJob.ResourceType), "#" + newJob.anchor);
    var jobStatusInfo = { 'resourceType': link, 'status': "starting ..." };
    var currJobStatus = isSimpleJob ? $('#simpleStatusInfoTmpl').tmpl(jobStatusInfo) : $('#jobStatusInfoTmpl').tmpl(jobStatusInfo);
    $('#infotop #statusInfo').append(currJobStatus);
    validatorApp.currJobNote = jQuery('#status', currJobStatus);

    //+ customize click evh to expand the target automatically
    jQuery("a[href]", currJobStatus).click(function () {
        var h = $(this).attr('href');
        expandView($(h + ' .sources'));
        expandView($(h + ' .results'));
        return true;
    });

    var t = $('#resultTemplate').tmpl(newJob);
    jQuery('.resultbody', t).attr('id', newJob.anchor);
    t.appendTo('#infobody');
    jQuery('.sources img', t).click(function (ev) { evh_enableCollapse(ev); });
    jQuery('.results img', t).click(function (ev) { evh_enableCollapse(ev); });
    return t;
}

function newRegularValidation(newJob) {
    var t = newValidation(newJob, true);
    jQuery("#summary", t).hide();

    // to display infomational message when type is error payload, or
    // to display infomational message when type is other payload
    if (newJob.ResourceType == 5) {
        jQuery("#informational", t).html("<p>" + validatorConf.info_error_resourse + "</p>");
    } else if (newJob.ResourceType >= 6 && newJob.ResourceType <=8) {
        jQuery("#informational", t).html("<p>" + validatorConf.info_other_odata_resourse + "</p>");
    } else if (newJob.ResourceType == 99) {
        jQuery("#informational", t).html("<p>" + validatorConf.info_other_resourse + "</p>");
    }

    return t;
}

function newCrawlingValidation(newJob) {
    var t = newValidation(newJob, false);
    // for crawling validation job to collapse areas by default
    enableCollapse(jQuery('.sources', t))
    enableCollapse(jQuery('.results', t))
    return t;
}

function submitLiveValidationJob() {
    var request = {
        method: "GET",
        requestUri: "odatavalidator/UriValidationJobs"
            + "?Uri='" + encodeURIComponent(GetServiceToValidateUri()).replace(new RegExp("'", "g"), "%25%32%37")
            + "'&Format='" + encodeURIComponent(GetSelectedFormat()) + "'"
            + curateReqHeaders()
    };
    alertSendoutRequest();
    OData.request(request, function (data) {
        alertReceiveResponse();
        $.each(data.results, function () {
            var job = { 'Uri': this.Uri, 'ResourceType': this.ResourceType, 'Id': this.DerivativeJobId, 'RuleCount': this.RuleCount, 'Issues': this.Issues };
            validatorApp.jobsInQ.push(job);
        });

        $('#infotop #statusInfo').empty();
        startJob(true);
    });
}

function curateReqHeaders() {
    var str = "";
    if ($('#v3Compliance').is(':checked')) {
        str = "&Headers='" + 'minDataServiceVersion:3.0;' + "'";
    }
    return str;
}

function startJob(isSimpleJob) {
    while (validatorApp.jobsInQ.length > 0) {
        resetJob();
        validatorApp.currentJob = validatorApp.jobsInQ.shift();
        if (validatorApp.currentJob.Issues) {
            //display status message only, w/o anchor
            var jobStatusInfo = { 'resourceType': validatorApp.currentJob.Uri, 'status': validatorApp.currentJob.Issues };
            $('#jobStatusInfoTmpl').tmpl(jobStatusInfo).appendTo('#infotop #statusInfo');
            continue;
        }

        validatorApp.currJobDetail = (isSimpleJob) ? newRegularValidation(validatorApp.currentJob) : newCrawlingValidation(validatorApp.currentJob);
        LoadPayload(validatorApp.currentJob.Id);
        return;
    }
    OnEnd();
}

function submitCrawlingValidationJob() {
    var request = {
        method: "GET",
        requestUri: "odatavalidator/UriValidationJobs"
            + "?Uri='" + encodeURIComponent(GetServiceToValidateUri())
            + "'&Format='" + encodeURIComponent(GetSelectedFormat()) + "'"
            + "&toCrawl='yes'"
            + curateReqHeaders()
    };
    alertSendoutRequest();
    OData.request(request, function (data) {
        alertReceiveResponse();
        $.each(data.results, function () {
            var job = { 'Uri': this.Uri, 'ResourceType': this.ResourceType, 'Id': this.DerivativeJobId, 'RuleCount': this.RuleCount, 'Issues': this.Issues };
            validatorApp.jobsInQ.push(job);
        });

        $('#infotop #statusInfo').empty();
        startJob();
    });
}

function LoadPayload(jobID) {
    alertPayloadInProgress();
    var query = "odatavalidator/ValidationJobs(guid'" + jobID + "')/PayloadLines?$orderby=LineNumber";
    OData.read(query, function (data) {
        $.each(data.results, function () {
            AddNewSourceLine(this);
        });
        UpdateLoadedResultCount(0, true);
        LoadResults(jobID, 0);
    });
}

function LoadResults(jobID, totalResultsRetrieved) {
    alertResultsInProgress();
    var query = "odatavalidator/ValidationJobs(guid'" + jobID + "')/TestResults?$orderby=ID";
    if (totalResultsRetrieved > 0) { query += "&$skip=" + totalResultsRetrieved };

    OData.read(query, function (data) {
        $.each(data.results, function () {
            AddNewResult(this);
            totalResultsRetrieved++;
        });
        UpdateLoadedResultCount(totalResultsRetrieved);
        LoadMoreResults(jobID, totalResultsRetrieved);
    });
}

function LoadMoreResults(jobID, totalResultsRetrieved) {
    if (totalResultsRetrieved < validatorApp.currentJob.RuleCount) {
        validatorStat.totalRequestsSent++;
        if (validatorStat.totalRequestsSent < validatorConf.maxRequests) {
            validatorApp.LoadResultsTimeoutObj = setTimeout(function () { LoadResults(jobID, totalResultsRetrieved) }, validatorConf.perRequestDelay);
        } else {
            alertTimeout();
            enableTabSwitch();
        }
    } else {
        onJobEnd();
        startJob();
    }
}

//event handlers
function evh_enableCollapse(ev) {
    enableCollapse(ev.currentTarget.parentNode);
}

function enableCollapse(ta) {
    var timg = jQuery('img', ta)[0];
    timg.src = (timg.nameProp == "arrow_right.gif") ? "Images/arrow_down.gif" : "Images/arrow_right.gif";
    jQuery('#display', ta).toggle();
}

function expandView(ta) {
    jQuery('img', ta)[0].src = "Images/arrow_down.gif";
    jQuery('#display', ta).show();
}

function collapseView(ta) {
    jQuery('img', ta)[0].src = "Images/arrow_right.gif";
    jQuery('#display', ta).hide();
}

// display housekeeping methods
function ClearDisplay() {
    $('#infobody').empty();
    $('#infotop #statusInfo').empty();
}

function AddNewSourceLine(line) {
    var srclist = jQuery('.sources #display', validatorApp.currJobDetail);
    srclist.append(CreateNewSourceLineHTML(line));
}

function AddNewResult(testResult) {
    (validatorApp.currentJob.Id == testResult.ValidationJobId)
    {
        if (testResult.Classification == "error") validatorApp.results.errors++;
        else if (testResult.Classification == "warning") validatorApp.results.warnings++;
        else if (testResult.Classification == "recommendation") validatorApp.results.recommendations++;
        else if (testResult.Classification == "success") validatorApp.results.successes++;
        else if (testResult.Classification == "notApplicable") { validatorApp.results.notApplicables++; return; }
        else if (testResult.Classification == "aborted") { validatorApp.results.aborteds++; return; }
        HighlightSourceLine(testResult.LineNumberInError);
        jQuery(".results #display", validatorApp.currJobDetail).append(CreateTestResultHtml(testResult));
    }
}

function categorizedMessage(isFirst, count, nounce) {
    var str = "";
    if (count > 0) {
        str = isFirst ? " " : ", ";
        str += count + " " + nounce;
        if (count != 1) str += "s";
    }
    return str;
}

function getIssusText() {
    var s = categorizedMessage(true, validatorApp.results.errors, "error");
    s += categorizedMessage(!s, validatorApp.results.warnings, "warning");
    s += categorizedMessage(!s, validatorApp.results.recommendations, "recommendation");
    return s;
}

function updateResultCountMessage() {
    var applicableRules = validatorApp.currentJob.RuleCount - validatorApp.results.notApplicables;
    var executedRules = applicableRules - validatorApp.results.aborteds;
    var statusLine = "(" + executedRules + " of " + applicableRules + " rules executed."
    var resultCountMsg = getIssusText();

    if (resultCountMsg) {
        statusLine += " " + resultCountMsg + "."
    }

    statusLine += ")";
    jQuery("#resultsCount", validatorApp.currJobDetail).text(statusLine);
    if (resultCountMsg && validatorApp.currJobNote) {
        if (jQuery("#issueCount", validatorApp.currJobNote.parent())) {
            jQuery("#issueCount", validatorApp.currJobNote.parent()).text(resultCountMsg + ".");
        }
    }
}

function jobHasAnyIssue() {
    return validatorApp.results.errors > 0 || validatorApp.results.warnings > 0 || validatorApp.results.recommendations > 0;
}

function UpdateLoadedResultCount(resultsLoaded, onFirstLoad) {
    if (onFirstLoad) {
        DisplayStatusText("Submitting Validation Task...");
    } else {
        DisplayStatusText("Loaded " + resultsLoaded + " of " + validatorApp.currentJob.RuleCount + " validation results");
    }
}

function summarize() {
    if (validatorApp.results.aborteds > 0) return "Some rules were not executed. Issue has been logged and will be investigated. You can see a list of all rules " + CreateLink("here", "roadmap.htm") + ".";
    if (validatorApp.results.errors == 0 && validatorApp.results.warnings == 0) {
        if (validatorApp.results.recommendations <= 0) {
            return "Congratulations! This is a valid OData endpoint. You can see a list of all rules " + CreateLink("here", "roadmap.htm#rules") + ".";
        } else {
            return "Congratulations! This is a valid OData endpoint, but interoperability with the largest number of OData clients may be improved by implementing the following recommendations."
                    + "You can see a list of all rules " + CreateLink("here", "roadmap.htm#rules") + ".";
        }
    }
    return "You can see a list of all rules " + CreateLink("here", "roadmap.htm#rules") + ".";
}

function displayJobSummary() {
    jQuery("#completionMessage", validatorApp.currJobDetail).empty().append("<p>" + summarize() + "</p>");
    updateResultCountMessage();
}

function DisplayResultsSections(isVisible) {
    if (isVisible) {
        $("#resultsArea").show();
        $("#sourceArea").show();
    } else {
        $("#resultsArea").hide();
        $("#sourceArea").hide();
    }
}


function updateJobStatus(txt) {
    validatorApp.currJobNote.empty().text(txt);
}

function DisplayStatusText(txt) {
    validatorApp.currJobNote ? updateJobStatus(txt) : $('#simpleStatusInfoTmpl').tmpl({ 'status': txt }).appendTo('#infotop #statusInfo');
}

function HighlightSourceLine(lineNumber) {
    if (lineNumber < 0) return;
    jQuery("#sourceLine" + lineNumber, validatorApp.currJobDetail).css("background-color", "#FFEC8E");
}

function CreateNewSourceLineHTML(line) {
    var resultLine = "<li class=\"sourceLine\" id=\"sourceLine" + line.LineNumber + "\">";
    resultLine += CreateAnchor("pane" + validatorApp.panes + "line" + line.LineNumber);
    resultLine += htmlEncode(line.LineNumber + ": " + line.LineText);
    resultLine += "</li>";
    return resultLine;
}

function CreateTestResultHtml(testResult) {
    var resultLine = "<li class=\"" + GetResultItemClass(testResult.Classification) + "\">";
    if (testResult.LineNumberInError && testResult.LineNumberInError >= 0)
        resultLine += "[" + CreateLink(testResult.LineNumberInError, "#pane" + validatorApp.panes + "line" + testResult.LineNumberInError) + "] ";
    resultLine += htmlEncode(testResult.Description) + " ";
    if (testResult.SpecificationUri) { resultLine += "[" + CreateLinkOfNewWindow("Specification", validatorConf.SpecificationUri) + "] [Section: " + testResult.SpecificationUri + "]"; }
    if (testResult.HelpUri) { resultLine += "[" + CreateLink("More Info", testResult.SpecificationUri) + "] "; }
    resultLine += "</li>";
    return resultLine;
}

function htmlEncode(value) {
    return $('<div/>').text(value).html();
}

function CreateAnchor(value) {
    return "<a name=\"" + value + "\"></a>";
}

function CreateLink(description, uri) {
    return "<a href=\"" + uri + "\">" + description + "</a>";
}

function CreateLinkOfNewWindow(description, uri) {
    return "<a href=\"" + uri + "\" target=\"_blank\" >" + description + "</a>";
}

function GetSelectedFormat() {
    return $("input[@name=formatGroup]:checked").val();
}

function GetServiceToValidateUri() {
    return $("#odataUri").val();
}

function GetPayloadText() {
    if ($("#payload_text").val() == validatorConf.payload_text_help) { return ''; }
    return $("#payload_text").val();
}

function GetMetadataText() {
    if ($("#meta_text").val() == validatorConf.meta_text_help) { return ''; }
    return $("#meta_text").val();
}

function GetReqHeaders() {
    var version = $('input[name=odataversion]:checked').val();
    if (!version) {
        version = '*'; 
    }

    return 'DataServiceVersion: ' + version + ';';
}

function GetResultItemClass(resultType) {
    if (resultType == "error") return "errorResult";
    if (resultType == "warning") return "warningResult";
    if (resultType == "success") return "successResult";
    if (resultType == "recommendation") return "recommendationResult";
}


function ToggleView(viewID, iconID) {
    if ($(iconID)[0].nameProp == "arrow_right.gif")
        $(iconID)[0].src = "Images/arrow_down.gif";
    else
        $(iconID)[0].src = "Images/arrow_right.gif";
    $(viewID).toggle();
}

function ToTypeDesc(type) {
    if (type == 0) return "none";
    if (type == 1) return "Service Document";
    if (type == 2) return "Metadata Document";
    if (type == 3) return "OData Feed";
    if (type == 4) return "OData Entry";
    if (type == 5) return "Error Payload";
    return "other";
}